#!/usr/bin/python3
# -*-coding:utf-8 -*

"""  Fichier regroupant les fonctions, classes de manipulation des maps """

# IMPORTS
from os.path import join
from operator import add
from pygame import Surface,Rect,SRCALPHA

from config import MAP_FOLDER
from config import EXT_MAP
from config import COLON,COMMA
from config import MOVEMENT,OPPOSITE_DIRECTION,UP,DOWN,RIGHT,LEFT
from config import TILE_WIDTH,TILE_HEIGHT
from utils import getCoordXY
from graphicalComponents import GC

class Map (object):

    """
        Classe de gestion d'une carte du jeu
        
        Paramètres:
        str mapName - nom de la carte
        int mapCols - nombre de colonne de la carte
        int mapRows - nombre de ligne de la carte
        list matrixMapTiles - dictionnaire regroupant les cases du jeu
            exemple : [ [MapTile,MapTile,...], [MapTile,MapTile,...], ... ]
        Rect mapRect - Contour de la carte
        
    """

    def __init__(self,mapName):
        """
            Constructeur de la classe
            
            Paramètres nommées :
            str mapName - nom de la carte
        
        """
        # Variable
        self.mapName=mapName
        # Chargement des cases de la carte
        self.__loadTiles(mapName)

    def __loadTiles(self,mapName):
        """
            Méthode de chargement des cases de la carte
            
            Paramètres nommées :
            str mapName - nom de la carte
        
        """
        # Chemin vers la map
        pathMap=join(MAP_FOLDER,'%s.%s'%(mapName,EXT_MAP))
        # Dictionnaire associatif : {(x,y) : 'tilesetName:elementName',(x,y) : 'tilesetName:elementName',...)
        dictMapTile={}
        # Variable pour la taille de la map
        colsMax,rowsMax=0,0

        # Ouverture du fichier de la nouvelle map
        try:
            fileMap=open(pathMap,'rU')
        except IOError,message:
            print 'Impossible d\'ouvrir le fichier carte : %s.%s'%(mapName,EXT_MAP)
            raise SystemExit,message

        # Traitement de la nouvelle map
        for line in fileMap:
            # Suppression des caractères spéciaux
            line=line.strip()
            # Traitement des lignes non vide
            if len(line)>0:
                # Extraction des informations
                infoLine=line.split()
                # Gestion du type d'élément de la carte
                # TODO : gerer autres éléments que les cases : transition et sprite
                coord=getCoordXY(infoLine[0],COMMA)
                # Récupération des coordonnées max pour la taille de la map
                colsMax,rowsMax=max(coord[0],colsMax),max(coord[1],rowsMax)
                # Ajout de l'élément au dictionnaire des cases pour la création des objet MapTile
                dictMapTile[(coord[0],coord[1])]=infoLine[1:]

        # Sauvegarde du nombre de colonnes et ligne de la carte
        # Remarque : ajout d'un aux lignes et colonnes pour compenser le fait que l'on parte de 0 dans les coordonnées de la carte
        self.mapRows=rowsMax+1
        self.mapCols=colsMax+1
        # Contour de la carte
        self.mapRect=Rect((0,0),(self.mapCols*TILE_WIDTH,self.mapRows*TILE_HEIGHT))
        # Création des objets cases 
        self.matrixMapTiles=self.__createMapTiles(dictMapTile)

    def __createMapTiles(self,dictMapTile):
        """
            Fonction de création de la matrice des cases d'une map
            
            Paramètres nommés :
            dict dictMapTile - dictionnaire des cases issus du fichier map.
                exemple : {(0, 1): ['exemple:wall-middle-bottom', 'exemple:top-tree'],
                           (1, 2): ['exemple:wall-middle'],
                           ...
                           }        
            Retour :
            tableau( tableau (MapTile, MapTile...),
                     tableau (MapTile, MapTile...),
                     ...
                    )
        """
        # initialisation de la matrice de la carte
        mapTiles=[[MapTile(x,y) for y in range(self.mapRows)] for x in range(self.mapCols)]

        # Parcourt du dictionnaire en vu de la mise à jour de la matrice
        for coord,listInfoPoint in dictMapTile.items():
            for strInfoPoint in listInfoPoint:
                # Extraction des informations du point
                tilesetName,elementTilesetName=strInfoPoint.split(COLON)
                # Récupération du contexte graphique
                tileset=GC.getitem(tilesetName,'tileset')
                # Ajout de l'élément à la case
                mapTiles[coord[0]][coord[1]].addTile(tileset[elementTilesetName])

        # Retour
        return mapTiles

    def loadImage(self,listLevel):
        """
            Méthode de chargement de l'image de la carte
            
            Paramètre nommé:
            list listLevel - liste des niveaux de l'image désiré
            
            Retour:
            dict - { (coordCol, coordRow) : Surface, (coordCol, coordRow) : Surface, ...}
            
        """
        # Dictionnaire de retour
        retour={}
        # Ajout des images des cases sur la carte  
        for line in self.matrixMapTiles:
            for tile in line:
                # Récupération de l'image
                tileImage=tile.getImage(listLevel)
                if tileImage:
                    # Ajout 
                    retour[(tile.col*TILE_WIDTH,tile.row*TILE_HEIGHT)]=tileImage

        # Gestion du canal de transparance
        return retour

    def show(self,showRect):
        """
            Fonction retournant la partie de la carte à visualiser.
            
            Paramètre nommée:
            rect showRect - objet rectangle pygame
            
            Retour:
            Surface
            
        """
        # Création et retour d'une partie de la carte
        return self.mapImage.subsurface(showRect)

    def canCross(self,spriteRect,direction):
        """
            Fonction permettant de vérifier si la case sur laquelle le sprite veut se déplacer est traversable
            
            Paramètre nommés:
            rect spriteRect - rectangle de representation du sprite
            int direction - clé de la direction du sprite
            
            Retour:
            bool
             
        """
        # Récupération du nouveau rectangle et de la case actuelle du sprite
        tupleSprite=self.__isNewSpriteInMap(spriteRect,direction)
        if not tupleSprite:
            return False
        newRect,spriteTile=tupleSprite[0],tupleSprite[1]

        # Gestion traversabilité
        listCrossedMapTiles=[]
        for mapTile in self.__getTileMapCollide(newRect):
            # Ajout de la traversabilité de la case à la liste
            if spriteTile==mapTile:
                listCrossedMapTiles.append(mapTile.canCross(direction))
            else:
                listCrossedMapTiles.append(mapTile.canCross(OPPOSITE_DIRECTION[direction]))
        # Retour
        return not False in listCrossedMapTiles

    def __isNewSpriteInMap(self,spriteRect,direction):
        """
            Fonction de détection de la presence dans la carte du prochain mouvement du sprite
            
            Paramètres nommés:
            Rect spriteRect - rectangle représentant la surface du sprite
            int direction - clé de la direction du sprite
            
            Retour:
            tuple - nouveau rectangle définittion le sprite
                    case sur laquelle se trouve l'ancien sprite
            
        """
        # Création des coordonnées
        oldRect=Rect(spriteRect.midleft,(spriteRect.width,spriteRect.height/2))
        newRect=Rect(tuple(map(add,spriteRect.midleft,MOVEMENT[direction])),(spriteRect.width,spriteRect.height/2))
        # Détection des bords de la carte
        if not self.mapRect.contains(newRect):
            return None
        else:
            # Récupéraion de la case sur laquelle se trouve le sprite
            spriteTile=self.__getSpriteTile(oldRect)
            if not spriteTile:
                raise SystemExit,'Détection de la case du sprite impossible.'
            return newRect,spriteTile

    def __getSpriteTile(self,rect):
        """
            Fonction de récupération de la case sur laquel se trouve la majorité du rectangle du sprite
            
            Paramètres nommés:
            Rect rect - rectangle représentant le sprite
            
            Retour:
            MapTile

        """
        for line in self.matrixMapTiles:
            for tile in line:
                if tile.containSprite(rect):
                    return tile
        return None

    def __getTileMapCollide(self,rect):
        """
            Fonction de récupération des cases traversé par le sprite
            
            Paramètres nommés:
            Rect rect - rectangle représentant le sprite
            
            Retour:
            List(MapTile, MapTile)
            
        """
        # Variable
        listMapTiles=[]
        # Parcours des cases de la carte
        for line in self.matrixMapTiles:
            for tile in line:
                if rect.colliderect(tile.rect):
                    # Ajout de la traversabilité de la case à la liste
                    listMapTiles.append(tile)
        # Retour
        return listMapTiles

    def __repr__(self):
        """
            Fonction de représentation de l'objet Map
            
            Retour :
            string
            
        """
        message="MAP : %s\n"%(self.mapName)
        for line in self.matrixMapTiles:
            for tile in line:
                message+="%s "%(str(tile))
            message+="\n"
        return message

    def __str__(self):
        """
            Fonction appelée quand on souhaite afficher le dictionnaire grâce
            à la fonction 'print' ou le convertir en chaîne grâce au constructeur
            'str'. On redirige sur __repr__
            
            Retour :
            string
        
        """
        return repr(self)

class MapTile (object):

    """
        Classe représentant une case sur une carte.
        
        Atributs :
        int col - abscisse de la case.
        int row - ordonnée de la case. 
        list graphicalTile - liste des objets GraphicalTile de la case.
        Rect rect - rectangle de représentation de la case
        
    """

    def __init__(self,col,row):
        """
            Constructeur de la classe.
            
            Paramètres nommés :
            int col - abscisse de la case.
            int row - ordonnée de la case. 
            
        """
        self.col=col
        self.row=row
        self.listGraphicalTiles=[]
        self.rect=Rect((self.col*TILE_WIDTH,self.row*TILE_HEIGHT)
                      ,(TILE_WIDTH,TILE_HEIGHT)
                      )
    def addTile(self,graphicalTile):
        """
            Métode d'ajout d'un élément graphique à notre liste pour la case
            
            Paramètre nommés:
            GraphicalTile graphicalTile - objet graphicalTile issu d'un tileset

        """
        self.listGraphicalTiles.append(graphicalTile)

    def getImage(self,listLevel):
        """
            Fonction de récupération de l'image de la case
            
            Paramètre nommé:
            list listLevel - liste des niveaux de l'image désiré
            
            Retour:
            Surface
            
        """
        if len(self.listGraphicalTiles)==0:
            raise SystemExit,'Pas d\'image définit pour la case %s-%s'%(self.col,self.row)
        else:
            # Création d'un objet sur lequel on va superposer les images
            # Utilisation du flag SRCALPHA et de la conversion de l'image avec alpha pour la gestion de la transparance
            image=Surface((TILE_WIDTH,TILE_HEIGHT),SRCALPHA).convert_alpha()
            # Superposition des images de la liste
            retour=False
            for graphicalTile in self.listGraphicalTiles:
                if graphicalTile.level in listLevel:
                    retour=True
                    image.blit(graphicalTile.image,(0,0))
            # Retour uniquement si on a des images
            if not retour:
                return None
            return image

    def canCross(self,direction):
        """
            Fonction permettant de savoir si la case est traversable
            
            Paramètre nommé:
            int direction - clé de la direction du sprite
            
            Retour:
            Bool
            
        """
        # Parcourt de tous les éléments graphiques pour savoir si il y en a un de non traversable
        for graphicalTiles in self.listGraphicalTiles:
            if not graphicalTiles.dictCross[direction]:
                return False
        return True

    def getPosition(self):
        """
            Fonction de récupération des coordonnées haut-gauche, bas-droite de la case
            
            Retour:
            tuple coordonnées - ((haut-gauche-x, haut-gauche-y), (bas-droite-x,bas-droite-y))
            
        """
        return (self.col*TILE_WIDTH,self.row*TILE_HEIGHT),((self.col+1)*TILE_WIDTH,(self.row+1)*TILE_HEIGHT)

    def containSprite(self,rect):
        """
            Fonction permettant de déterminer si un sprite est sur la case
            
            Paramètre nommé:
            Rect rect - rectangle représentant le sprite
            
            Retour:
            Bool
            
        """
        # Gestion de l'intersection des deux rectangles
        inter=self.rect.clip(rect)
        # Cas ou il n'y a pas d'intersection
        if inter.size==(0,0):
            return False
        # True si inter est d'au moins 50%
        return  (inter.width*inter.height)/(self.rect.width*self.rect.height)>=0,5

    def __ne__(self,mapTile):
        """
            Fonction de comparaison de notre objet avec !=
            
            Paramètres nommés :
            MapTile mapTile - case de la carte à comparer avec notre objet
            
            Retour :
            booléen
        
        """
        return not self==mapTile

    def __eq__(self,mapTile):
        """
            Fonction de comparaison de notre objet avec ==
            
            Paramètres nommés :
            MapTile mapTile - dictionnaire à comparer avec notre objet
            
            Retour :
            booléen
        
        """
        # Contrôle de l'entrée
        if type(mapTile) not in [MapTile]:
            raise TypeError('Une case ne peut être comparée qu\'avec une autre case')
        return (self.col==mapTile.col) and (self.row==mapTile.row)

    def __repr__(self):
        """
            Fonction de représentation de l'objet MapTile
            
            Retour :
            string
            
        """
        return '<Tile (%s,%s) %s>'%(self.col,self.row,self.listGraphicalTiles)

    def __str__(self):
        """
            Fonction appelée quand on souhaite afficher le dictionnaire grâce
            à la fonction 'print' ou le convertir en chaîne grâce au constructeur
            'str'. On redirige sur __repr__
            
            Retour :
            string
        
        """
        return repr(self)
